﻿using ServiceStack.Net30.Collections.Concurrent;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Threading.Tasks;

namespace RedisDemo
{

    public static class EntityDictionaryMapper
    {
        private delegate T mapEntityWithPrefix<T>(Dictionary<string, string> dic, string prefix);
        private static ConcurrentDictionary<Type, Delegate> cachedMappers_Entity = new ConcurrentDictionary<Type, Delegate>();

        private delegate Dictionary<string, string> mapDictionaryWithPrefix<T>(T entity, string prefix);
        private static ConcurrentDictionary<Type, Delegate> cachedMappers_Dict = new ConcurrentDictionary<Type, Delegate>();
        private static ConcurrentDictionary<Type, string> cached_Properties = new ConcurrentDictionary<Type, string>();

        public static List<T> ToEntityList<T>(this Dictionary<string, string> dic)
        {
            return MapToEntityList<T>(dic);
        }

        public static List<T> MapToEntityList<T>(Dictionary<string, string> dic)
        {
            List<T> entityList = new List<T>();
            Type entityType = typeof(T);


            string propName = string.Empty;
            if (!cached_Properties.TryGetValue(entityType, out propName))
            {
                propName = string.Empty;
                PropertyInfo[] props = entityType.GetProperties();

                for (int i = 0; i < props.Length; i++)
                {
                    PropertyInfo pi = props[i];
                    Type propertyType = pi.PropertyType;
                    Type nullableType = Nullable.GetUnderlyingType(propertyType);
                    bool isNullable = nullableType != null;
                    Type underlyingType = nullableType ?? pi.PropertyType;
                    if (underlyingType.IsPrimitive || underlyingType == typeof(string))
                    {
                        propName = (pi.Name.Length > propName.Length) ? pi.Name : propName;
                    }
                }
                cached_Properties.TryAdd(entityType, propName);
            }


            IList<string> prefixList = dic.Where(d => d.Key.EndsWith(propName)).Select(d => d.Key.Replace(propName, string.Empty)).ToList();
            for (int i = 0; i < prefixList.Count; i++)
            {
                Dictionary<string, string> shard = dic.Where(d => d.Key.StartsWith(prefixList[i])).ToDictionary(kv => kv.Key, kv => kv.Value);
                T entity = MapToEntity<T>(shard, prefixList[i]);
                entityList.Add(entity);
            }

            return entityList;
        }


        public static T ToEntity<T>(this Dictionary<string, string> dic)
        {
            return MapToEntity<T>(dic);
        }

        public static T MapToEntity<T>(Dictionary<string, string> dic)
        {
            return MapToEntity<T>(dic, string.Empty);
        }

        public static T ToEntity<T>(this Dictionary<string, string> dic, string prefix)
        {
            return MapToEntity<T>(dic, prefix);
        }
        public static T MapToEntity<T>(Dictionary<string, string> dic, string prefix)
        {

            // Get a delegate reference to the dynamic method
            mapEntityWithPrefix<T> invokeMapEntity = (mapEntityWithPrefix<T>)CodeGen_Entity<T>();//cachedMappers_Entity[typeof(T)];
            return invokeMapEntity(dic, prefix);
        }

        public static Delegate CodeGen_Entity<T>()
        {
            Type genericType = typeof(T);
            Delegate invokeMap;
            // If a mapping function from dictionary -> T does not exist, create and cache one
            if (!cachedMappers_Entity.TryGetValue(genericType, out invokeMap))
            {
                // Our method will take in 2 parameters, a dictionary and a string as prefix
                Type[] methodArgs = { typeof(Dictionary<string, string>), typeof(string) };

                Type dictionaryType = typeof(Dictionary<string, string>);
                Type stringType = typeof(string);

                // The MapDictionary method will map a Dictionary to an instance of type T
                DynamicMethod dm = new DynamicMethod("MapDictionaryToPocoWithPrefix", genericType, methodArgs, Assembly.GetExecutingAssembly().GetType().Module);
                ILGenerator il = dm.GetILGenerator();

                // We'll have a single local variable, the instance of T we're mapping
                il.DeclareLocal(genericType);

                // Create a new instance of T and save it as variable 0
                il.Emit(OpCodes.Newobj, genericType.GetConstructor(Type.EmptyTypes));
                il.Emit(OpCodes.Stloc_0);



                foreach (PropertyInfo pi in genericType.GetProperties())
                {
                    Type nullableType = Nullable.GetUnderlyingType(pi.PropertyType);
                    bool isNullable = nullableType != null;
                    Type underlyingType = nullableType ?? pi.PropertyType;
                    //none primitive type or string skip mapping this property
                    if (!underlyingType.IsPrimitive && underlyingType != typeof(string))
                    {
                        continue;
                        // throw new ArgumentException("members of entity must be primitive type, nullable primitive type or string");
                    }


                    LocalBuilder local_key = il.DeclareLocal(stringType);
                    //prefix stores in argument 1
                    il.Emit(OpCodes.Ldarg_1);
                    il.Emit(OpCodes.Ldstr, pi.Name);
                    il.Emit(OpCodes.Call, stringType.GetMethod("Concat", new Type[] { stringType, stringType }));
                    il.Emit(OpCodes.Stloc_S, local_key);


                    Label lblNull = il.DefineLabel();

                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Ldloc_S, local_key);
                    il.Emit(OpCodes.Callvirt, dictionaryType.GetMethod("ContainsKey", new Type[] { stringType }));
                    il.Emit(OpCodes.Brfalse, lblNull);

                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Ldloc_S, local_key);
                    // Push the dictionary entry value onto the stack
                    il.Emit(OpCodes.Callvirt, typeof(Dictionary<string, string>).GetMethod("get_Item", new Type[] { typeof(string) }));

                    //save dictionary entry value as variable 1
                    il.Emit(OpCodes.Stloc_1);
                    il.Emit(OpCodes.Ldloc_1);
                    il.Emit(OpCodes.Call, typeof(string).GetMethod("IsNullOrWhiteSpace", new Type[] { typeof(string) }));
                    //save bool result as variable 2
                    il.Emit(OpCodes.Stloc_2);
                    il.Emit(OpCodes.Ldloc_2);
                    //if value is empty, ignore
                    il.Emit(OpCodes.Brtrue, lblNull);
                    //otherwise reload instance of T (0) and dictionary entry value (1) onto stack
                    il.Emit(OpCodes.Ldloc_0);
                    il.Emit(OpCodes.Ldloc_1);

                    // Depending on the type of the property, convert the dictionary entry value to the type
                    switch (underlyingType.Name)
                    {
                        case "Int16":
                            il.Emit(OpCodes.Call, typeof(Convert).GetMethod("ToInt16", new Type[] { typeof(string) }));
                            if (isNullable)
                            {
                                il.Emit(OpCodes.Newobj, typeof(Nullable<Int16>).GetConstructor(new Type[] { typeof(Int16) }));
                            }
                            break;
                        case "Int32":
                            il.Emit(OpCodes.Call, typeof(Convert).GetMethod("ToInt32", new Type[] { typeof(string) }));
                            if (isNullable)
                            {
                                il.Emit(OpCodes.Newobj, typeof(Nullable<Int32>).GetConstructor(new Type[] { typeof(Int32) }));
                            }
                            break;
                        case "Int64":
                            il.Emit(OpCodes.Call, typeof(Convert).GetMethod("ToInt64", new Type[] { typeof(string) }));
                            if (isNullable)
                            {
                                il.Emit(OpCodes.Newobj, typeof(Nullable<Int64>).GetConstructor(new Type[] { typeof(Int64) }));
                            }
                            break;
                        case "Boolean":
                            il.Emit(OpCodes.Call, typeof(Convert).GetMethod("ToBoolean", new Type[] { typeof(string) }));
                            if (isNullable)
                            {
                                il.Emit(OpCodes.Newobj, typeof(Nullable<Boolean>).GetConstructor(new Type[] { typeof(Boolean) }));
                            }
                            break;
                        case "String":
                            il.Emit(OpCodes.Ldstr, string.Empty);
                            il.Emit(OpCodes.Call, typeof(String).GetMethod("Concat", new Type[] { typeof(string), typeof(string) }));
                            break;
                        case "DateTime":
                            il.Emit(OpCodes.Call, typeof(Convert).GetMethod("ToDateTime", new Type[] { typeof(string) }));
                            if (isNullable)
                            {
                                il.Emit(OpCodes.Newobj, typeof(Nullable<DateTime>).GetConstructor(new Type[] { typeof(DateTime) }));
                            }
                            break;
                        case "Decimal":
                            il.Emit(OpCodes.Call, typeof(Convert).GetMethod("ToDecimal", new Type[] { typeof(string) }));
                            if (isNullable)
                            {
                                il.Emit(OpCodes.Newobj, typeof(Nullable<Decimal>).GetConstructor(new Type[] { typeof(Decimal) }));
                            }
                            break;
                        default:
                            // Don't set the field value as it's an unsupported type
                            continue;
                    }

                    // Set the T instances property value
                    il.Emit(OpCodes.Callvirt, genericType.GetMethod("set_" + pi.Name, new Type[] { pi.PropertyType }));

                    il.MarkLabel(lblNull);
                }

                // Load the T instance onto the stack
                il.Emit(OpCodes.Ldloc_0);

                // Return
                il.Emit(OpCodes.Ret);

                invokeMap = dm.CreateDelegate(typeof(mapEntityWithPrefix<T>));
                // Cache the method so we won't have to create it again for the type T
                cachedMappers_Entity.TryAdd(genericType, invokeMap);
            }
            return invokeMap;
        }

        public static Dictionary<string, string> MapToDictionary<T>(T entity, params string[] properties)
        {
            return MapToDictionary<T>(entity, string.Empty, properties);
        }

        public static Dictionary<string, string> ToDictionary<T>(this T entity, params string[] properties)
        {
            return MapToDictionary<T>(entity, string.Empty, properties);
        }

        public static Dictionary<string, string> ToDictionary<T>(this T entity, string prefix, params string[] properties)
        {
            // Get a delegate reference to the dynamic method
            mapDictionaryWithPrefix<T> invokeMapDictionary = (mapDictionaryWithPrefix<T>)CodeGen_Dictionary<T>(properties);
            return invokeMapDictionary(entity, prefix);
        }
        public static Dictionary<string, string> MapToDictionary<T>(T entity, string prefix, params string[] properties)
        {
            // Get a delegate reference to the dynamic method
            mapDictionaryWithPrefix<T> invokeMapDictionary = (mapDictionaryWithPrefix<T>)CodeGen_Dictionary<T>(properties);
            return invokeMapDictionary(entity, prefix);
        }
        

        public static Delegate CodeGen_Dictionary<T>(params string[] properties)
        {

            Type genericType = typeof(T);
            Delegate invokeMap;
            // If a mapping function from T -> Dictionary<string,string> does not exist, create and cache one
            if (!cachedMappers_Dict.TryGetValue(genericType, out invokeMap))
            {
                // Our method will take in a single parameter, a POCO entity of T
                Type[] methodArgs = { genericType, typeof(string) };

                Type dictionaryType = typeof(Dictionary<string, string>);
                Type stringType = typeof(string);

                // The MapPOCO method will map a Dictionary  to an instance of type T
                DynamicMethod dm = new DynamicMethod("MapPOCO", typeof(Dictionary<string, string>), methodArgs, Assembly.GetExecutingAssembly().GetType().Module);
                ILGenerator il = dm.GetILGenerator();
                // We'll have a single local variable, the instance of dictionary  we're mapping
                LocalBuilder local_dic = il.DeclareLocal(typeof(Dictionary<string, string>));


                // Create a new instance of dictionary and save it as variable 0
                il.Emit(OpCodes.Newobj, typeof(Dictionary<string, string>).GetConstructor(Type.EmptyTypes));
                il.Emit(OpCodes.Stloc_S, local_dic);
                

                //local variable to store nullable property hasvalue check result
                LocalBuilder local_hasValue = il.DeclareLocal(typeof(bool));
                //local variable to store value before writing dictionary entry
                LocalBuilder local_stringValue = il.DeclareLocal(typeof(string));

                //il.BeginExceptionBlock();

                if (properties.Length == 0)
                {
                    properties = genericType.GetProperties().Select(p => p.Name).ToArray();
                }


                foreach (string pname in properties)//(PropertyInfo pi in typeof(T).GetProperties())
                {
                    il.Emit(OpCodes.Ldnull);
                    il.Emit(OpCodes.Stloc_S, local_stringValue);

                    PropertyInfo pi = genericType.GetProperty(pname);
                    Type propertyType = pi.PropertyType;
                    Type nullableType = Nullable.GetUnderlyingType(propertyType);
                    bool isNullable = nullableType != null;
                    Type underlyingType = nullableType ?? pi.PropertyType;
                    //not primitive type or string skip mapping this property
                    if (!underlyingType.IsPrimitive && underlyingType != typeof(string))
                    {
                        continue;
                    }


                    // Load the dictionary instance, T parameter and the field name onto the stack
                    il.Emit(OpCodes.Ldloc_0);
                    il.Emit(OpCodes.Ldstr, pi.Name);
                    il.Emit(OpCodes.Ldarg_0);
                    //Push the property value onto the stack 
                    il.Emit(OpCodes.Callvirt, genericType.GetMethod("get_" + pi.Name, Type.EmptyTypes));

                    Label lblNull = il.DefineLabel();
                    //type of this property is nullable
                    if (isNullable)
                    {
                        //local variable to store nullable int property
                        LocalBuilder local_Nullable = il.DeclareLocal(propertyType);
                        il.Emit(OpCodes.Stloc_S, local_Nullable);
                        il.Emit(OpCodes.Ldloca_S, local_Nullable);
                        il.Emit(OpCodes.Call, propertyType.GetProperty("HasValue").GetGetMethod());
                        //save 'HasValue' check result as variable local_hasValue 
                        il.Emit(OpCodes.Stloc_S, local_hasValue);
                        //load onto stack for judgement
                        il.Emit(OpCodes.Ldloc_S, local_hasValue);

                        //if nullable object is null,jump to lblNull
                        il.Emit(OpCodes.Brfalse, lblNull);

                        //get value of this nullable property
                        il.Emit(OpCodes.Ldloca_S, local_Nullable);
                        il.Emit(OpCodes.Callvirt, propertyType.GetMethod("get_Value", Type.EmptyTypes));
                    }
                    if (underlyingType == typeof(string))
                    {
                        il.Emit(OpCodes.Ldstr, string.Empty);
                        il.Emit(OpCodes.Call, typeof(String).GetMethod("Concat", new Type[] { typeof(string), typeof(string) }));
                    }
                    else
                    {
                        il.Emit(OpCodes.Call, typeof(Convert).GetMethod("ToString", new Type[] { underlyingType }));
                    }

                    il.Emit(OpCodes.Stloc_S, local_stringValue);

                    //if nullable property is null jump here
                    il.MarkLabel(lblNull);

                    LocalBuilder local_key = il.DeclareLocal(typeof(string));
                    //prefix stores in argument 1
                    il.Emit(OpCodes.Ldarg_1);
                    il.Emit(OpCodes.Ldstr, pi.Name);
                    il.Emit(OpCodes.Call, typeof(String).GetMethod("Concat", new Type[] { typeof(string), typeof(string) }));
                    il.Emit(OpCodes.Stloc_S, local_key);
                    //load required variables onto stack for creating dictionary entry
                    il.Emit(OpCodes.Ldloc_0);
                    il.Emit(OpCodes.Ldloc_S, local_key);
                    il.Emit(OpCodes.Ldloc_S, local_stringValue);

                    il.Emit(OpCodes.Callvirt, typeof(Dictionary<string, string>).GetMethod("set_Item", new Type[] { typeof(string), typeof(string) }));
                }

                il.Emit(OpCodes.Ldloc_0);

                // Return
                il.Emit(OpCodes.Ret);

                invokeMap = dm.CreateDelegate(typeof(mapDictionaryWithPrefix<T>));
                // Cache the method so we won't have to create it again for the type T
                cachedMappers_Dict.TryAdd(genericType, invokeMap);
            }
            return invokeMap;
        }

        public static void ClearCachedMapperMethods()
        {
            cachedMappers_Entity.Clear();
            cachedMappers_Dict.Clear();
        }
    }
}
